/*
 * Copyright 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.beepmystuff;

import android.app.Application;

import com.google.beepmystuff.BmsClient.SessionStorage;

import org.apache.http.impl.client.DefaultHttpClient;

/** A static factory for creating dependencies. */
public abstract class Injector {
  private static Injector instance = null;
  private static final Injector TEST_DEATH_SENTINEL = new TestDeathInjector();

  /**
   * Returns the singleton instance of Injector that was configured via
   * {@link #configureForTest(Injector)}, or a default injector if none has
   * been configured.
   *
   * @throws IllegalStateException if no injector has been configured.
   */
  public static synchronized Injector getInjector(Application application) {
    if (instance == null) {
      setInjector(new DefaultInjector(application));
    }
    return instance;
  }

  /**
   * Sets the singleton injector to return from
   * {{@link #getInjector(Application)}.
   *
   * @throws IllegalStateException if an injector has already been configured.
   */
  private static void setInjector(Injector injector) {
    if (instance != null) {
      throw new IllegalStateException("An injector has already been configured");
    }
    instance = injector;
  }

  /**
   * Configures this class to return a provided injector.
   *
   * @throws IllegalStateException if an injector has already been configured.
   */
  public static synchronized void configureForTest(Injector injector) {
    // We want to catch the case where a testcase hasn't called resetForTest()
    // in its tearDown() method, so we only clear the current instance if it
    // was a TestDeathInjector sentinel.  If not, setInjector() will throw an
    // ISE.
    if (instance == TEST_DEATH_SENTINEL) {
      instance = null;
    }
    setInjector(injector);
  }

  /**
   * Resets the provided injector so that another test can call
   * {@link #configureForTest(Injector)}.
   *
   * @throws IllegalStateException if no injector has been configured.
   */
  public static synchronized void resetForTest() {
    if (instance == null) {
      throw new IllegalStateException("No injector has been configured");
    }
    instance = TEST_DEATH_SENTINEL;
  }

  /**
   * Returns the default injector that was created by calling
   * {@link #getInjector(Application)}, allowing calling code to inject some
   * dependencies (but retain the default creation for others).
   *
   * @throws IllegalStateException if the default injector is not in use.
   */
  public DefaultInjector asDefault() {
    if (this instanceof DefaultInjector) {
      return (DefaultInjector) this;
    }
    throw new IllegalStateException("Default injector is not in use");
  }

  public abstract Preferences getPreferences();
  public abstract SessionStorage getSessionStorage();
  public abstract BmsHttpClient getBmsHttpClient();
  public abstract BmsLowLevelClient getBmsLowLevelClient();

  /**
   * An implementation of {@link Injector} that creates dependencies on-demand
   * using a provided Application context.
   *
   * Individual dependencies can be replaced using methods provided on this
   * class.
   */
  public static class DefaultInjector extends Injector {
    // This apikey is one allocated to me (Matt Godbolt) for this client
    // only. Please do not use it for another project, instead email
    // beepmaster@beepmystuff.com for your own key!
    private static final String BMSAPIKEY = "hRYBzzQ9LUqNMMHB3A8BUfeg0uuGRgEY";

    private final Application application;

    private BmsHttpClient bmsHttpClient;
    private BmsLowLevelClient bmsLowLevelClient;
    private Preferences preferences;
    private SessionStorage sessionStorage;

    private DefaultInjector(Application application) {
      this.application = application;
    }

    @Override
    public synchronized BmsHttpClient getBmsHttpClient() {
      if (bmsHttpClient == null) {
        bmsHttpClient = new DefaultBmsHttpClient(new DefaultHttpClient());
      }
      return bmsHttpClient;
    }

    public synchronized void setBmsHttpClient(BmsHttpClient bmsHttpClient) {
      this.bmsHttpClient = bmsHttpClient;
    }

    @Override
    public synchronized BmsLowLevelClient getBmsLowLevelClient() {
      if (bmsLowLevelClient == null) {
        bmsLowLevelClient = new DefaultBmsLowLevelClient(getBmsHttpClient(), BMSAPIKEY);
      }
      return bmsLowLevelClient;
    }

    public synchronized void setBmsLowLevelClient(BmsLowLevelClient bmsLowLevelClient) {
      this.bmsLowLevelClient = bmsLowLevelClient;
    }

    @Override
    public synchronized Preferences getPreferences() {
      if (preferences == null) {
        createSharedPreferences();
      }
      return preferences;
    }

    public synchronized void setPreferences(Preferences preferences) {
      this.preferences = preferences;
    }

    @Override
    public synchronized SessionStorage getSessionStorage() {
      if (sessionStorage == null) {
        createSharedPreferences();
      }
      return sessionStorage;
    }

    public synchronized void setSessionStorage(SessionStorage sessionStorage) {
      this.sessionStorage = sessionStorage;
    }

    private void createSharedPreferences() {
      SharedPreferences sharedPreferences = new SharedPreferences(application);
      if (preferences == null) {
        preferences = sharedPreferences;
      }
      if (sessionStorage == null) {
        sessionStorage = sharedPreferences;
      }
    }
  }

  /**
   * An implementation of {@link Injector} that fails all calls.
   *
   * This is used as a sentinel during tests to inhibit the creation of a
   * default injector.
   */
  private static class TestDeathInjector extends Injector {
    @Override
    public BmsHttpClient getBmsHttpClient() {
      throw new UnsupportedOperationException();
    }

    @Override
    public BmsLowLevelClient getBmsLowLevelClient() {
      throw new UnsupportedOperationException();
    }

    @Override
    public Preferences getPreferences() {
      throw new UnsupportedOperationException();
    }

    @Override
    public SessionStorage getSessionStorage() {
      throw new UnsupportedOperationException();
    }
  }
}
